package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.*;
import com.hmdp.entity.User;
import com.hmdp.entity.UserInfo;
import com.hmdp.mapper.UserAccountMapper;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    //注入Redis数据库template
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //注入UserAccountMapper
    @Autowired(required = false)
    private UserAccountMapper userAccountMapper;

    //注入UserInfoServiceImpl
    @Autowired(required = false)
    private UserInfoServiceImpl userInfoService;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1. 校验手机号(利用提前写好的工具类进行校验)
        if (RegexUtils.isPhoneInvalid(phone)){
            //2. 如果不符合，返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //3. 如果符合，生成一个验证码(这里采用在pom.xml中引入的hutool工具类)
        String code = RandomUtil.randomNumbers(6);
//        //4. 保存验证码到session
//        session.setAttribute(SystemConstants.CODE,code);
        //4. 保存验证码到redis中(添加业务前缀来区分，设置key的有效期，减少数据库内存消耗) set key value ex 120
        stringRedisTemplate.opsForValue().set(SystemConstants.LOGIN_CODE_KEY + phone, code, SystemConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);

        //5. 发送验证码(需要调用第三方的平台，比如阿里云的短信平台)
        log.debug("发送短信验证码成功, 验证码：{}",code);// 通过日志模拟发送验证码成功
        //返回 OK
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        //1. 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            //2. 如果不符合，返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2. 校验验证码，与session中的验证码进行对比
//        String cacheCode = (String) session.getAttribute(SystemConstants.CODE);
//        String code = loginForm.getCode();
//        if(cacheCode == null || !cacheCode.equals(code)){
//            //3. 不一致，报错(session验证码过期，或验证码错误)
//            return Result.fail("验证码过期或错误!");
//        }

        //TODO 2. 从redis中获取手机验证码并验证
        String cacheCode = stringRedisTemplate.opsForValue().get(SystemConstants.LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.equals(code)){
            //3. 不一致，报错(session验证码过期，或验证码错误)
            return Result.fail("验证码过期或错误!");
        }
        //4. 一直，根据手机号查询用户 select * from tb_user where phone
        /**
         * extends ServiceImpl<UserMapper, User>
         * 这是Mybatis-Plus提供的单表增删改查，可以在业务层中使用父类中的方法进行CURD
         * User user = query().eq("phone", phone).one();表示查单个用户
         * List<User> userList = query().eq("phone", phone).list();表示差多个用户
         * 但是数据库中tb_user对应的phone字段是唯一字段值unique，因此只能单个用户
         */
        User user = query().eq("phone", phone).one();
        //5. 判断用户是否存在
        if(user == null){
            //6. 不存在，创建新用户并保存
            /**
             * 这里密码可以置空，用户在没有设置密码前可以使用手机短信登录
             * 昵称先初始化为随机字符串
             */
            user = createUserWithPhone(phone);
        }

        //7. 保存用户信息到session中(需要做敏感信息保护，减少内存消耗)
//        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);//采用hutool工具类，将user里面的信息拷贝到UserDTO里面去
//        session.setAttribute(SystemConstants.USER, userDTO);
        //TODO 7. 保存用户信息到session中
        //TODO 7.1 生成token作为登录凭证
        String token = UUID.randomUUID().toString();
        //TODO 7.2 将UserDTO对象转换为HashMap存储(使用hutool工具类)
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create()
            .setIgnoreNullValue(true)
            .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())
        );
        //TODO 7.3 存储，设置数据有效期
        stringRedisTemplate.opsForHash().putAll(SystemConstants.LOGIN_TOKEN_PRE + token,userMap);
        //TODO 7.4 设置token有效期
        stringRedisTemplate.expire(SystemConstants.LOGIN_TOKEN_PRE + token, SystemConstants.LOGIN_TOKEN_TTL, TimeUnit.MINUTES);
        //TODO token 有效期不是一旦到了30分钟就过期，如果30分钟以内访问过该数据，则时效重新刷新为30分钟,该功能在拦截器ReflashTokenIntercetor实现

        // 8.UV统计
        // 8.1.获取当前日期
        LocalDateTime now_date = LocalDateTime.now();
        // 8.2.将日期格式化，作为Redis中UV的后缀
        String now_suffix = now_date.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        String key_uv = UV_KEY_PRE + now_suffix;
        // 8.3.向数据库写入
        stringRedisTemplate.opsForHyperLogLog().add(key_uv, String.valueOf(userDTO.getId()));

        //8. 返回token
        return Result.ok(token);
    }

    @Override
    public void logout() {
        //删除本地用户，在第二个拦截器中，如果本地用户获取为null，则返回401信息
        UserHolder.removeUser();
        // TODO 删除redis中用户对应的token
//        stringRedisTemplate.opsForValue()
    }

    @Override
    public Result queryUserByid(Long userId) {
        User user = this.getById(userId);
        if(user == null){
            return Result.ok();
        }
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        return Result.ok(userDTO);
    }

    // 用户签到
    @Override
    public Result sign() {
        // 1.获得当前登录用户
        Long userId = UserHolder.getUser().getId();
        // 2.获取当前日期信息
        LocalDateTime now = LocalDateTime.now();
        // 3.拼接Key
        String keySuffix = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
        String key = SIGN_KEY_PRE + userId + keySuffix;
        // 4.获取今天是本月的第几天
        int dayOfMonth = now.getDayOfMonth(); //月的一天，从1到31
        // 5.写入Redis，SETBIT key offset 1
        stringRedisTemplate.opsForValue().setBit(key, dayOfMonth-1, true);
        // 6.更新积分
        // 6.1.统计连续签到天数
        int days = cc(key, dayOfMonth);
        // 6.2.获取用户积分
        String op_str = stringRedisTemplate.opsForValue().get(USER_POINT_PRE + userId);
        int op = Integer.parseInt(op_str);
        op += (days % 7) * 10;
        // 6.3.将新数据写回redis用户
        stringRedisTemplate.opsForValue().set(USER_POINT_PRE + userId, String.valueOf(op));
        // 7.用户签到的时候，还要重新统计连续签到的天数
        Result result = signCount();
        return result;
    }

    //提炼判断连续签到天数的代码
    public int cc(String key, int dayOfMonth){
        // 5.从Redis获取本月截止今天为止的所有签到数据(返回是一个10进制的数字) BITFIELD sign:101020224 GET u18 0
        List<Long> result = stringRedisTemplate.opsForValue().bitField(
                key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0L)
        ); //因为该命令可以同时GET SET INCRBY， 因此结果是一个集合
        if(result == null || result.isEmpty()){
            //没有任何签到结果
            return 0;
        }
        Long num = result.get(0);
        System.out.println("签到数据的十进制是: " + num);
        if(num == null || num == 0){
            return 0;
        }
        // 6.逐个bit位右移逻辑与，判断连续签到的天数
        int count = 0;
        while(true){
            //判断这个bit位是否为0
            if((num & 1) == 0){
                //如果为0，说明未签到，结束
                break;
            }
            //如果不为0，说明已签到，计数器+1
            count++;
            //把数字右移一位，抛弃最后一个bit位，继续下一个bit位
            num >>>= 1; //>>：表示有符号右移，>>>：表示无符号右移
        }
        return count;
    }

    //统计连续签到的天数以及当天是否已经签到,更新积分
    @Override
    public Result signCount() {
        // 1.获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        // 2.获取当前日期所在月份的第几天
        LocalDateTime now = LocalDateTime.now();
        int dayOfMonth =now.getDayOfMonth(); //这里不是角标，因为无需减1
        // 3.拼接Key
        String keySuffix = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
        String key = SIGN_KEY_PRE + userId + keySuffix;
        // 4.判断当天是否已经签到
        SignResult r = new SignResult();
        Boolean hasSigned = stringRedisTemplate.opsForValue().getBit(key, dayOfMonth - 1);
        if(hasSigned == null || hasSigned == false){
            r.setHasSigned(false);
        }else{
            r.setHasSigned(true);
        }
        // 5.从Redis获取本月截止今天为止的所有签到数据(返回是一个10进制的数字) BITFIELD sign:101020224 GET u18 0
        List<Long> result = stringRedisTemplate.opsForValue().bitField(
                key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0L)
        ); //因为该命令可以同时GET SET INCRBY， 因此结果是一个集合
        if(result == null || result.isEmpty()){
            //没有任何签到结果
            return Result.ok(r);
        }
        Long num = result.get(0);
        System.out.println("签到数据的十进制是: " + num);
        if(num == null || num == 0){
            return Result.ok(r);
        }
        // 6.逐个bit位右移逻辑与，判断连续签到的天数
        int count = 0;
        while(true){
            //判断这个bit位是否为0
            if((num & 1) == 0){
                //如果为0，说明未签到，结束
                break;
            }
            //如果不为0，说明已签到，计数器+1
            count++;
            //把数字右移一位，抛弃最后一个bit位，继续下一个bit位
            num >>>= 1; //>>：表示有符号右移，>>>：表示无符号右移
        }
        r.setCount(count);
        // 7.查询用户积分
        String op = stringRedisTemplate.opsForValue().get(USER_POINT_PRE + userId);
        r.setPoint(Integer.parseInt(op));
        return Result.ok(r);
    }

    /**
     * 开通或续费会员，不需要用到Redisson，因为后期会限制不能重复登录用户
     * @param len 开通或续费月数
     * @return
     */
    @Override
    public Result openOrRenewalVIP(int len) {
        // 1.获取本地用户的id
        Long userId = UserHolder.getUser().getId();
        // 2.查询数据库获取当前用户的积分
        int point = userAccountMapper.pointOfAccountById(userId);
        String str_point = String.valueOf(point);
        // 3.查询缓存判断当前用户是否是VIP，使用获取key的expire方法
        String key = "user:vip:" + userId;
        Long expire = stringRedisTemplate.opsForValue().getOperations().getExpire(key); //返回单位为秒，-1表示没设置expire，-2表示不存在该key
        long mmo = 2678400L * len; // timeout of a month
        if(expire == null || expire == -2){
            // 3.1.不是，开通（不存在该值，表示还没有开通）
            // 一个月的秒数是31*24*60*60=2678400
            stringRedisTemplate.opsForValue().set(key,str_point, mmo, TimeUnit.SECONDS);
            return Result.ok("开通VIP成功！");
        }else{
            // 3.2.是，续费，重置过期时间
            long newTimeout = expire + mmo;
            stringRedisTemplate.opsForValue().set(key,str_point,newTimeout,TimeUnit.SECONDS);
            return Result.ok("续费VIP成功！");
        }

    }

    /**
     * 用手机号快速创建用户
     * @param phone
     * @return
     */
    private User createUserWithPhone(String phone) {
        // 1.创建用户
        User user = new User();
        user.setPhone(phone);
//        user.getPassword();这里密码可以置空，用户在没有设置密码前可以使用手机短信登录
        //昵称先初始化为随机字符串
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        // 2.保存用户
        boolean save = save(user);
        // 3.创建Redis中的用户积分,初始积分为0
//        User one = this.query()
//                .eq("phone", user.getPhone()).one();
//        Long userId = one.getId();
//        stringRedisTemplate.opsForValue().set(USER_POINT_PRE + userId, "0");
        return user;
    }

    /**
     * 修改昵称
     * @param newNickname
     * @return
     */
    @Override
    public Result updateNickname(String newNickname) {
        // 1.获取当前用户id
        Long userId = UserHolder.getUser().getId();
        // 2.修改数据库资料
        boolean update = update().set("nick_name", newNickname).eq("id", userId.toString()).update();
        // 3.ThreadLocal中的实体需要修改
        if(update){
            // 3.1.获取本地线程的UserDTOPlus对象，更改重置nickname

            // 3.2.将UserDTOPlus对象存回于本地线程
        }
        return Result.ok("修改nickname成功！新的nickname: " + newNickname);
    }

    /**
     * 修改介绍
     * @param introduce
     * @return
     */
    @Override
    public Result updateIntroduce(String introduce) {
        // 1.获取当前用户id
        Long userId = UserHolder.getUser().getId();
        // 2.修改数据库资料
        boolean update = userInfoService.update().set("introduce", introduce).eq("user_id",userId).update();

        return Result.ok("修改introduce成功！新的introduce: " + introduce);

    }

    /**
     * 修改三项基本信息：性别、城市、生日
     * @param gender
     * @param city
     * @param birthday
     * @return
     */
    @Override
    public Result updateUserInfo(String gender, String city, String birthday) {
        // 1.获取当前用户id
        Long userId = UserHolder.getUser().getId();
        // 2.修改数据库资料
        Integer gd = gender == "男" ? 0 : 1; //将前端传入的字符串性别转为数字
        LocalDate bd = LocalDate.parse(birthday); //将前端传入的字符串日期格式化为LocalDate
        boolean update = userInfoService.update()
                .set("gender", gd)
                .set("city", city)
                .set("birthday", bd)
                .eq("user_id",userId).update();
        if(update){
            return Result.ok("三项基本信息修改成功!");
        }
        return Result.fail("三项基本信息修改失败!");
    }

    /**
     * 获取用户的基本信息，因为修改信息不是热点操作，因此暂时先从数据库读取信息
     * 待改进操作：在ThreadLocal存入map，key分为UserDTO和UserDTOPlus
     * @return
     */
    @Override
    public Result getBasicInfo() {
        // 1.获取本地用户id
        Long id = UserHolder.getUser().getId();
        // 2.查询数据库
        // 2.1.查询user表
        User user = this.query().eq("id", id.toString()).one();
        // 2.2.查询user_info 表
        UserInfo userInfo = userInfoService.query().eq("user_id", id.toString()).one();
        if(null == userInfo){
            //表示还没有设置个人基本信息
            return Result.ok();
        }
        // 2.3.结合信息到UserDTOPlus中
        UserDTOPlus userDTOPlus = new UserDTOPlus();
        userDTOPlus.setIcon(user.getIcon());
        userDTOPlus.setNickname(user.getNickName());
        userDTOPlus.setIntroduce(userInfo.getIntroduce());
        userDTOPlus.setGender(userInfo.getGender() ? "女" : "男");
//        userDTOPlus.setGender(userInfo.getGender());
        userDTOPlus.setCity(userInfo.getCity());
        userDTOPlus.setBirthday(userInfo.getBirthday());
        return Result.ok(userDTOPlus);
    }


}
